package com.noahsloan.nutils.net;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;

import com.noahsloan.nutils.log.Logger;

/**
 * Base class for classes that listen on a socket. A new instance can function
 * as a server by calling run(), although this class is designed to be part of a
 * larger server.
 * 
 * All responsibility for handling a new connection is handled by
 * handleConnection(Socket).
 * 
 * Implements Runnable so that the listener may be given to a thread pool.
 * 
 * In addition, several methods other than handleConnection(Socket) may be
 * overridden by sub-classes, although they do not have to be.
 * 
 * @see SocketListener#bindFailed()
 * @see SocketListener#handleSocketError(Socket, IOException)
 * @see SocketListener#handleSSError(ServerSocket, IOException)
 * 
 * These methods may not throw any checked exceptions, and should not throw
 * RuntimeExceptions. A general strategy for subclasses might be to utilize
 * these methods as listeners, to notify another thread of this server's
 * failure.
 * 
 * @author Noah Sloan
 * @see SocketListener#handleConnection(Socket)
 */
public abstract class SocketListener implements Runnable {

    private static final Logger LOG = Logger.getLogger(SocketListener.class);

    /**
     * the number of times in a row to try to restart the server socket
     */
    public static final int MAX_ATTEMPTS = Integer.getInteger(
            "n-utils.net.SocketListener.max-attempts", 15);

    private volatile boolean shutdown; // is this listener shutdown?

    private volatile ServerSocket ss;

    protected final SocketAddress address; // the socket address to bind to

    /**
     * Create a listener for the wildcard address and given port.
     * 
     * @param port
     */
    public SocketListener(int port) {
        this(new InetSocketAddress(port));
    }

    /**
     * Create a listener for address:port
     * 
     * @param address
     * @param port
     */
    public SocketListener(String address, int port) {
        this(new InetSocketAddress(address, port));
    }

    /**
     * Create a listener on the given socket address.
     * 
     * @param address
     */
    public SocketListener(SocketAddress address) {
        this.address = address;
    }

    /**
     * Creates a server socket that listens on the port provided in the
     * constructor. Will attempt to re-bind to the given socket address up to
     * MAX_ATTEMPTS times.
     * 
     * @see SocketListener#MAX_ATTEMPTS
     */
    public void run() {
        boolean canBind = true;
        int bindAttempts = 0;
        while (!shutdown && canBind) {
            LOG.info("Starting Server Socket. Binding to " + address);
            // Assume that we cannot bind until we successfully get a
            // connection from a client
            canBind = false;
            try {
                // create a new socket and bind to it
                ss = new ServerSocket();
                ss.setReuseAddress(true);
                ss.bind(address);

                // as long as we are bound, and not shutdown, handle
                // connections
                while (!shutdown && ss.isBound()) {
                    Socket socket = ss.accept();
                    canBind = true;// we have successfully made a connection
                    try {
                        handleConnection(socket);
                    } catch (IOException e) {
                        handleSocketError(socket, e);
                    }
                }
            } catch (IOException e) {
                handleSSError(ss, e);
            }
            // reset bind attempts if we successfully bound, otherwise
            // count the
            // attempts
            bindAttempts = canBind ? 0 : bindAttempts + 1;
            // shutdown if we fail to bind too many times
            shutdown = shutdown || bindAttempts > MAX_ATTEMPTS;
        }
        // print an error message if this shutdown was because of a
        // failure to bind
        if (bindAttempts > MAX_ATTEMPTS) {
            bindFailed();
        }
    }

    /**
     * Prints an error message and attempts to close the ServerSocket. May be
     * overridden by subclasses but may not throw any new checked exceptions.
     * 
     * @param ss
     *            the ServerSocket that threw the exception.
     * @param e
     *            the exception thrown.
     */
    protected void handleSSError(ServerSocket ss, IOException e) {
        LOG.info("Server Socket Error");
        try {
            ss.close();
        } catch (IOException e1) {
            // if it wont close, we can't do anything about it
            LOG.error("Error closing socket", e1);
        }
    }

    /**
     * Prints an error message and attempts to close the socket. May be
     * overridden by subclasses but may not throw any new checked exceptions.
     * 
     * @param socket
     *            the socket that threw the exception.
     * @param e
     *            the exception thrown.
     */
    protected void handleSocketError(Socket socket, IOException e) {
        LOG.warn(String
                .format("Error on socket to %s", socket.getInetAddress()), e);
        try {
            socket.close();
        } catch (IOException e1) {
            // can't do anything if it wont close...
            LOG.warn("Error closing socket", e1);
        }
    }

    /**
     * Logs an error message when the server socket fails to bind. May be
     * overridden by sub-classes, but may not throw any checked exceptions.
     */
    protected void bindFailed() {
        LOG.error(String.format("%s could not (re)bind to %s after"
                + " %d consecutive attempts.\n", this.getClass().getName(),
                address, MAX_ATTEMPTS));
    }

    /**
     * Handle is expected to start a new thread and/or quickly return so it can
     * handle the next incoming connection.
     * 
     * @param socket
     *            the socket that just connected.
     * @throws IOException
     *             if there is an error on the socket.
     */
    protected abstract void handleConnection(Socket socket) throws IOException;

    /**
     * Shutdown this listener. Note that this will not stop any servant threads
     * you may have spawned, it is up to you to ensure that they terminate
     * reasonably.
     * 
     * @throws IOException
     *             if an error occurs closing the socket.
     * 
     */
    public void shutdown() throws IOException {
        shutdown = true;
        ss.close();
    }

    public static void main(String[] args) {
        new SocketListener(Integer.getInteger("socketListenerPort", 1029)) {
            @Override
            // dumps data to std out
            protected void handleConnection(Socket socket) throws IOException {
                new com.noahsloan.nutils.InToOut(socket.getInputStream(), System.out,
                        true, false).connect();
            }
        }.run();
    }
}
